﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace Entropy.Ext.Ast
{
    public abstract class Node
    {
        internal void FindAndReplace<T>(Func<T, bool> selector, Func<T, Node> replace) where T : Node
        {
            FindAndReplaceIn(this, n => (n is T) && selector((T)n), n => replace((T)n));
        }
        internal void FindAndReplace(Func<Node, bool> selector, Func<Node, Node> replace)
        {
            FindAndReplaceIn(this, selector, replace);
        }

        private static void FindAndReplaceIn(object me, Func<Node, bool> selector, Func<Node, Node> replace)
        {
            foreach (var field in Fields(me))
            {
                if (IsNode(field.FieldType))
                {
                    var value = (Node)field.GetValue(me);
                    if (value != null)
                    {
                        FindAndReplaceIn(value, selector, replace);
                        if (selector(value))
                            field.SetValue(me, replace(value));
                    }
                }
                else if (field.FieldType.IsArray)
                {
                    var array = (Array)field.GetValue(me);
                    for (int i = 0; i < array.Length; i++)
                    {
                        var value = array.GetValue(i);
                        if (value != null)
                        {
                            FindAndReplaceIn(value, selector, replace);
                            var node = value as Node;
                            if (node != null && selector(node))
                                array.SetValue(replace(node), i);
                        }
                    }
                }
                else if (field.FieldType.IsClass && field.FieldType != typeof(string))
                {
                    var value = field.GetValue(me);
                    if (value != null)
                        FindAndReplaceIn(value, selector, replace);
                }
            }
        }

        private static FieldInfo[] Fields(object me)
        {
            return me.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        }

        internal IEnumerable<Node> Descendants()
        {
            foreach (var field in Fields(this))
            {
                if (IsNode(field.FieldType))
                {
                    yield return (Node)field.GetValue(this);
                }
                else if (field.FieldType.IsArray && IsNode(field.FieldType.GetElementType()))
                {
                    var array = (Array)field.GetValue(this);
                    for (int i = 0; i < array.Length; i++)
                    {
                        yield return (Node)array.GetValue(i);
                    }
                }
            }
        }

        public string Dump
        {
            get
            {
                var sb = new StringBuilder();
                DoDump(sb, "");
                return sb.ToString();
            }
        }

        private void DoDump(StringBuilder sb, string offset)
        {

            sb.AppendLine(GetType().ToString());
            if (this is Void)
                return;

            sb.AppendLine(offset + "{");
            offset += "    ";


            foreach (var field in GetType().GetFields())
            {
                sb.Append(offset + field.Name + ": ");
                if (IsNode(field.FieldType))
                {
                    ((Node)field.GetValue(this)).DoDump(sb, offset);
                }
                else if (field.FieldType.IsArray && IsNode(field.FieldType.GetElementType()))
                {
                    sb.AppendLine("[");
                    offset += "    ";
                    var array = (Array)field.GetValue(this);
                    for (int i = 0; i < array.Length; i++)
                    {
                        sb.Append(offset);
                        ((Node)array.GetValue(i)).DoDump(sb, offset);
                    }
                    offset = offset.Remove(offset.Length - 4, 4);
                    sb.AppendLine(offset + "]");
                }
                else
                {
                    sb.AppendLine(field.GetValue(this).ToString());
                }
            }
            offset = offset.Remove(offset.Length - 4, 4);
            sb.AppendLine(offset + "}");
        }

        private static bool IsNode(Type type)
        {
            return typeof(Node).IsAssignableFrom(type);
        }
    }
}